Skip to content

S03-08 JS-高级-事件循环、错误处理、Storage、正则、手写

[TOC]

事件循环@

概念

进程和线程

线程和进程是操作系统中的两个概念:

进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式;

线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中

通俗解释:听起来很抽象,这里还是给出我的解释:

  • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);

  • 线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;

  • 所以我们也可以说进程是线程的容器;

举例解释:再用一个形象的例子解释:

  • 操作系统类似于一个大工厂;

  • 工厂中里有很多车间,这个车间就是进程;

  • 每个车间可能有一个以上的工人在工厂,这个工人就是线程;

图解

image-20250423141933698

操作系统的工作方式

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

  • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换

  • 当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码;

  • 对于用户来说是感受不到这种快速的切换的;

你可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程:

image-20230620150803432

浏览器和JavaScript

JavaScript是单线程

我们经常会说JavaScript是单线程(可以开启workers) 的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。

浏览器是一个进程吗,它里面只有一个线程吗?

  • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;

  • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;

JS代码是在一个单独的线程中执行的

  • 这就意味着JavaScript的代码,在同一个时刻只能做一件事

  • 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞

所以真正耗时的操作,实际上并不是由JavaScript线程在执行的:

  • 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;

  • 比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可;

阻塞IO和非阻塞IO

如果我们希望在程序中对一个文件进行操作,那么我们就需要打开这个文件:通过文件描述符。

  • 我们思考:JavaScript 可以直接对一个文件进行操作吗?
  • 看起来是可以的,但是事实上我们任何程序中的文件操作都是需要进行系统调用(操作系统封装了文件系统);
  • 事实上对文件的操作,是一个操作系统的 IO 操作(输入、输出);

操作系统为我们提供了阻塞式调用和非阻塞式调用:

  • 阻塞式调用: 调用结果返回之前,当前线程处于阻塞态(阻塞态 CPU 是不会分配时间片的),调用线程只有在得到调用结果之后才会继续执行。
  • 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。

所以我们开发中的很多耗时操作,都可以基于这样的 非阻塞式调用

  • 比如网络请求本身使用了 Socket 通信,而 Socket 本身提供了 select 模型,可以进行非阻塞方式的工作
  • 比如文件读写的 IO 操作,我们可以使用操作系统提供的基于事件的回调机制

但是非阻塞 IO 也会存在一定的问题:我们并没有获取到需要读取(我们以读取为例)的结果

  • 那么就意味着为了可以知道是否读取到了完整的数据,我们需要频繁的去确定读取到的数据是否是完整的;
  • 这个过程我们称之为轮训操作;

那么这个轮训的工作由谁来完成呢?

  • 如果我们的主线程频繁的去进行轮训的工作,那么必然会大大降低性能;
  • 并且开发中我们可能不只是一个文件的读写,可能是多个文件;
  • 而且可能是多个功能:网络的 IO、数据库的 IO、子进程调用;

libuv 提供了一个线程池(Thread Pool):

  • 线程池会负责所有相关的操作,并且会通过轮训等方式等待结果;
  • 当获取到结果时,就可以将对应的回调放到事件循环(某一个事件队列)中;
  • 事件循环就可以负责接管后续的回调工作,告知 JavaScript 应用程序执行对应的回调函数;

Event loop in node.js

image-20240719155418018

阻塞和非阻塞,同步和异步有什么区别?

  • 阻塞和非阻塞是对于被调用者来说的;

    • 在我们这里就是系统调用,操作系统为我们提供了阻塞调用和非阻塞调用;
  • 同步和异步是对于调用者来说的;

    • 在我们这里就是自己的程序;
    • 如果我们在发起调用之后,不会进行其他任何的操作,只是等待结果,这个过程就称之为同步调用;
    • 如果我们再发起调用之后,并不会等待结果,继续完成其他的工作,等到有回调时再去执行,这个过程就是异步调用;

宏任务

宏任务(Macro Task): 是 JavaScript 事件循环中的一种异步任务类型,用于处理需要稍后执行的代码块。它的核心特点是:在事件循环的下一轮中执行,且优先级低于微任务

本质:代表一个独立的、完整的代码执行单元。

触发时机:在事件循环的每一轮(Tick)中,执行完当前所有微任务后,从宏任务队列中取出一个任务执行。

设计目的:处理非紧急任务(如延迟操作、I/O 回调、用户交互事件),避免阻塞主线程。

常见来源

  • JS 主代码块:初始的 <script> 标签代码(本质上是第一个宏任务)
  • 定时器setTimeoutsetInterval
  • I/O 操作:文件读写、网络请求(如 fetch 的回调)
  • DOM 事件clickscrollresize 等事件回调
  • UI 渲染:浏览器自动触发的渲染流程(如重绘、布局)
  • requestAnimationFrame:动画回调(部分浏览器将其归类为宏任务)

微任务

微任务(Micro Task):是 JavaScript 事件循环中优先级最高的异步任务类型,用于处理需要立即执行的高优先级操作。它的核心特点是:在当前宏任务执行完毕后、下一个宏任务开始前,一次性清空所有微任务

本质:代表一个需要尽快执行的轻量级任务。

触发时机:在每次宏任务执行结束后,立即清空微任务队列(包括嵌套生成的微任务)。

设计目的:处理需要即时响应的操作(如数据更新后的回调),确保在渲染前完成关键任务。

常见来源

  • Promise 回调Promise.then()Promise.catch()Promise.finally()
  • queueMicrotask:显式添加微任务:queueMicrotask(() => { ... })
  • MutationObserver:监听 DOM 变化的回调(如元素属性、子节点变动)
  • Node.js 环境特有process.nextTick()(优先级甚至高于普通微任务)

关键特性

  1. 高优先级:微任务队列的优先级高于宏任务队列,必须彻底清空后才会处理下一个宏任务。

  2. 完全清空:即使微任务中生成新的微任务(如嵌套 Promise.then),也会持续执行,直到队列为空。

  3. 渲染前执行:微任务在页面渲染前执行,适合处理需要即时生效的操作(如更新 DOM 后立即读取布局属性)。

示例

ts
console.log("1. 主线程开始");

// 宏任务
setTimeout(() => console.log("5. 宏任务"));

// 微任务
Promise.resolve().then(() => {
  console.log("3. 微任务");
  // 嵌套微任务
  Promise.resolve().then(() => console.log("4. 嵌套微任务"));
});

console.log("2. 主线程结束");

面试题:Promise 面试题

image-20230620150913872

image-20230620150922534

image-20230901171805611

面试题:Promise async await 面试题

image-20230620150931234

image-20230620150938885

image-20230907144738577

浏览器的事件循环

浏览器的事件循环(Event Loop):是 JavaScript 在单线程环境下实现异步编程的核心机制。它通过协调 调用栈任务队列渲染管道,确保代码执行不阻塞主线程,同时高效处理用户交互、网络请求和页面渲染。

它是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现。

核心组成

  1. 调用栈(Call Stack)
    • 按顺序执行同步代码(后进先出,LIFO)。
    • 当函数被调用时推入栈顶,执行完毕后弹出。
    • 若栈被长时间占用(如死循环),页面会卡死(阻塞)。
  2. 任务队列(Task Queues)
    • 宏任务队列(Macro Task Queue):存放 setTimeoutsetIntervalI/O、事件回调等任务。
    • 微任务队列(Micro Task Queue):存放 Promise.thenMutationObserverqueueMicrotask 等高优先级任务。
    • 其他队列:如 requestAnimationFrame 回调队列(与渲染相关)。
  3. 渲染管道(Rendering Pipeline)
    • 浏览器在合适的时机执行样式计算、布局(Layout)、绘制(Paint)等操作,更新页面显示。

事件循环的作用

  • 单线程的挑战:JavaScript 只有一个主线程,所有代码依次执行,若遇到耗时操作(如网络请求),页面会卡死。
  • 解决方案:事件循环将异步任务交给浏览器其他线程处理,任务完成后将回调放入队列,主线程空闲时按规则执行队列中的任务。

工作流程

事件循环的每一次迭代称为一个 “Tick”,其执行顺序如下:

  1. 执行一个宏任务

    • 从宏任务队列中取出最旧的任务(如初始的 script 代码块)。
    • 执行同步代码,遇到异步任务时:
      • 宏任务(如 setTimeout)的回调放入宏任务队列。
      • 微任务(如 Promise.then)的回调放入微任务队列。
  2. 清空微任务队列

    • 当前宏任务执行完毕后,立即依次执行微任务队列中的所有任务,直到队列为空
    • 注意:如果在处理微任务时又产生了新的微任务,会继续执行,直到彻底清空。
  3. 渲染页面(如果需要)

    • 浏览器根据刷新率(通常 60Hz,约 16.6ms/帧)决定是否渲染。
    • 执行与渲染相关的操作:
      • requestAnimationFrame 回调(在渲染前执行动画逻辑)。
      • 浏览器进行 样式计算 → 布局(Layout)→ 绘制(Paint)
      • 若时间充裕,可能执行 requestIdleCallback(空闲时处理低优先级任务)。
  4. 取下一个宏任务

    • 重复上述流程,形成循环。

image-20230620150844676

关键特性

  1. 微任务优先级高于宏任务

    • 每个宏任务执行后,必须清空所有微任务才会处理下一个宏任务。
  2. requestAnimationFrame 的定位

    • 其回调在渲染前执行,适合处理与动画相关的逻辑,不属于宏任务或微任务。
  3. 避免阻塞主线程

    • 长时间运行的同步代码(如大数据循环)会阻塞事件循环,导致页面无响应。
    • 优化方案:将任务拆分为多个小任务,通过 setTimeoutqueueMicrotask 分批执行。
  4. 宏任务的最小延迟

    • setTimeout(fn, 0) 的实际延迟至少为 4ms(浏览器规范限制)。

示例

ts
console.log("1. 主线程开始");

// 宏任务
setTimeout(() => console.log("4. 宏任务(setTimeout)"), 0);

// 微任务
Promise.resolve().then(() => console.log("3. 微任务(Promise)"));

console.log("2. 主线程结束");

Node的事件循环

Node的事件循环(Event Loop):是其非阻塞 I/O 和异步操作的核心机制,基于 libuv 库 实现。与浏览器的事件循环不同,Node 的事件循环采用 分阶段处理模型,将不同类型的任务分配到特定阶段执行。

libuv:是一个多平台的专注于异步IO的库,最初是为Node开发的,现在也被使用到Luvit、Julia、pyuv等其他地方。

图解

  • libuv 中主要维护了一个EventLoop和worker threads(线程池);
  • EventLoop 负责调用系统的一些其他操作:文件的IO、Network、child-processes等

image-20230620150957682

宏任务的六个阶段

Node事件循环会将宏任务按顺序执行以下阶段,每个阶段处理特定类型的任务:

  1. Timers:执行 setTimeout()setInterval() 的回调。
  2. Pending I/O:处理上一轮循环中延迟的 I/O 回调(如系统错误回调ECONNREFUSED)。
  3. Idle/Prepare:Node.js 内部使用的阶段(开发者一般无需关注)。
  4. Poll:检索新的 I/O 事件,执行 I/O 回调(如文件读取、网络请求),其他的宏任务基本都在此阶段执行。
  5. Check:执行 setImmediate() 的回调。
  6. Close Callbacks:执行关闭事件的回调(如 socket.on('close', ...))。

执行流程

text
┌───────────────────────┐
│        Timers         │ ← 执行到期的定时器回调(setTimeout/setInterval)
└──────────┬────────────┘

┌──────────▼────────────┐
│   Pending I/O Callbacks │ ← 执行系统操作(如TCP错误)的回调
└──────────┬────────────┘

┌──────────▼────────────┐
│     Idle/Prepare      │ ← Node.js 内部使用
└──────────┬────────────┘

┌──────────▼────────────┐
│         Poll          │ ← 等待新I/O事件,执行I/O回调
│                       │   如果队列为空:
│                       │     - 如有setImmediate,进入Check阶段
│                       │     - 否则等待新事件(阻塞)
└──────────┬────────────┘

┌──────────▼────────────┐
│        Check          │ ← 执行setImmediate回调
└──────────┬────────────┘

┌──────────▼────────────┐
│   Close Callbacks     │ ← 执行关闭事件的回调(如socket.close)
└───────────────────────┘

微任务的执行时机

Node 的微任务分为两种,执行优先级高于宏任务

  1. process.nextTick队列:在每个阶段结束后立即执行,优先级最高。
  2. 其他队列:如 Promise回调、queueMicrotask,它们会在 process.nextTick 队列清空后执行。

示例

ts
setTimeout(() => console.log('3. Timeout'), 0);
setImmediate(() => console.log('4. Immediate'));
Promise.resolve().then(() => console.log('2. Promise'));
process.nextTick(() => console.log('1. NextTick'));
// 注意:如果在I/O周期内初始化,可能先执行Immediate,再执行Timeout

面试题

面试题一

js
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout0')
}, 0)

setTimeout(function () {
  console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'))

process.nextTick(() => console.log('nextTick1'))

async1()

process.nextTick(() => console.log('nextTick2'))

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
  console.log('promise2')
}).then(function () {
  console.log('promise3')
})

console.log('script end')

执行结果如下:

script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3

setTimeout0
setImmediate
setTimeout2

面试题二

js
setTimeout(() => {
  console.log('setTimeout')
}, 0)

setImmediate(() => {
  console.log('setImmediate')
})

执行结果:

js
情况一:
setTimeout
setImmediate

情况二:
setImmediate
setTimeout

为什么会出现不同的情况呢?

  • 在 Node 源码的 deps/uv/src/timer.c 中 141 行,有一个 uv__next_timeout的函数;
  • 这个函数决定了,poll 阶段要不要阻塞在这里;
  • 阻塞在这里的目的是当有异步 IO 被处理时,尽可能快的让代码被执行;
js
int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  // 计算距离当前时间节点最小的计时器
  heap_node = heap_min(timer_heap(loop));
  // 如果为空, 那么返回-1,表示为阻塞状态
  if (heap_node == NULL)
    return -1; /* block indefinitely */

  // 如果计时器的时间小于当前loop的开始时间, 那么返回0
  // 继续执行后续阶段, 并且开启下一次tick
  handle = container_of(heap_node, uv_timer_t, heap_node);
  if (handle->timeout <= loop->time)
    return 0;

  // 如果不大于loop的开始时间, 那么会返回时间差
  diff = handle->timeout - loop->time;
  if (diff > INT_MAX)
    diff = INT_MAX;

  return (int) diff;
}

和上面有什么关系呢?

  • 情况一:如果事件循环开启的时间(ms)是小于 setTimeout函数的执行时间的;

    • 也就意味着先开启了 event-loop,但是这个时候执行到 timer 阶段,并没有定时器的回调被放到入 timer queue 中;
    • 所以没有被执行,后续开启定时器和检测到有 setImmediate 时,就会跳过 poll 阶段,向后继续执行;
    • 这个时候是先检测 setImmediate,第二次的 tick 中执行了 timer 中的 setTimeout
  • 情况二:如果事件循环开启的时间(ms)是大于 setTimeout函数的执行时间的;

    • 这就意味着在第一次 tick 中,已经准备好了 timer queue;
    • 所以会直接按照顺序执行即可;

throw、try catch

错误处理方案

开发中我们会封装一些工具函数,封装之后给别人使用:

  • 在其他人使用的过程中,可能会传递一些参数;

  • 对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果;

很多时候我们可能验证到不是希望得到的参数时,就会直接return:

  • 但是return存在很大的弊端:调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined;

  • 事实上,正确的做法应该是如果没有通过某些验证,那么应该让外界知道函数内部报错了;

如何可以让一个函数告知外界自己内部出现了错误呢?

  • 通过throw关键字,抛出一个异常

throw语句:

  • throw语句用于抛出一个用户自定义的异常

  • 当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行);

如果我们执行代码,就会报错,拿到错误信息的时候我们可以及时的去修正代码。

image-20230907151315727

image-20230907151319262

throw关键字

throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息:

image-20230620151503399

throw关键字可以跟上哪些类型呢?

  • 基本数据类型:比如number、string、Boolean

  • 对象类型:对象类型可以包含更多的信息

但是每次写这么长的对象又有点麻烦,所以我们可以创建一个类:

image-20230620151513806

image-20230907152324425

Error类型

事实上,JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象:

image-20230620151524499

Error包含三个属性:

  • messsage:创建Error对象时传入的message;

  • name:Error的名称,通常和类的名称一致;

  • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;

Error有一些自己的子类:**

  • RangeError:下标值越界时使用的错误类型;

  • SyntaxError:解析语法错误时使用的错误类型;

  • TypeError:出现类型错误时,使用的错误类型;

异常的处理

我们会发现在之前的代码中,一个函数抛出了异常,调用它的时候程序会被强制终止

  • 这是因为如果我们在调用一个函数时,这个函数抛出了异常,但是我们并没有对这个异常进行处理,那么这个异常会继续传递到上一个函数调用中

  • 而如果到了最顶层(全局)的代码中依然没有对这个异常的处理代码,这个时候就会报错并且终止程序的运行

我们先来看一下这段代码的异常传递过程:

  • foo函数在被执行时会抛出异常,也就是我们的bar函数会拿到这个异常;

  • 但是bar函数并没有对这个异常进行处理,那么这个异常就会被继续传递到调用bar函数的函数,也就是test函数;

  • 但是test函数依然没有处理,就会继续传递到我们的全局代码逻辑中;

  • 依然没有被处理,这个时候程序会终止执行,后续代码都不会再执行了;

image-20230620151542173

image-20230620151548898

异常的捕获

但是很多情况下当出现异常时,我们并不希望程序直接退出,而是希望可以正确的处理异常:

  • 这个时候我们就可以使用try catch

image-20230620151559919

image-20230620151614405

在ES10(ES2019)中,catch后面绑定的error可以省略。

当然,如果有一些必须要执行的代码,我们可以使用finally来执行:

  • finally表示最终一定会被执行的代码结构;

注意:如果try和finally中都有返回值,那么会使用finally当中的返回值;

Storage

正则表达式

防抖、节流

简介

防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中

  • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。

  • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题

但是很多前端开发者面对这两个功能,有点摸不着头脑:

  • 某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);

  • 某些开发者可以区分,但是不知道如何应用;

  • 某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;

接下来我们会一起来学习防抖和节流函数:

  • 我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;

  • 并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;

防抖函数

防抖函数(debounce)

我们用一副图来理解一下它的过程:

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;

  • 当事件密集触发时,函数的触发会被频繁的推迟

  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数

image-20230620152349381

应用场景:

防抖的应用场景很多:

  • 搜索联想oninput,输入框中频繁的输入内容,搜索或者提交信息;

  • 频繁点击事件onclick,频繁的点击按钮,触发某个事件;

  • 浏览器滚动事件onscroll,监听浏览器滚动事件,完成某些特定操作;

  • 浏览器缩放事件onresize,用户缩放浏览器的resize事件;

示例: 搜索联想

我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容

image-20230620152423458

比如想要搜索一个MacBook:

  • 当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;

  • 当继续输入ma时,再次发送网络请求;

  • 那么macbook一共需要发送7次网络请求;

  • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;

但是我们需要这么多次的网络请求吗?

  • 不需要,正确的做法应该是在合适的情况下再发送网络请求;

  • 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;

  • 比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;

  • 也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;

这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数

节流函数

节流函数(throttle)

我们用一副图来理解一下节流的过程

  • 当事件触发时,会执行这个事件的响应函数;

  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数

  • 不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;

image-20230620152442909

应用场景:

  • 页面滚动事件:监听页面的滚动事件;

  • 鼠标移动事件

  • 频繁点击事件:用户频繁点击按钮操作;

  • 游戏某些设计:游戏中的一些设计,如发射子弹;

很多人都玩过类似于飞机大战的游戏

在飞机大战的游戏中,我们按下空格会发射一个子弹:

  • 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;

  • 比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;

  • 但是事件是触发了10次的,响应的函数只触发了一次;

image-20230620152511039

生活中的例子

生活中防抖的例子:

比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。

如果在五分钟的时间内,没有同学问我问题,那么我就下课了;

  • 在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;

  • 如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);

生活中节流的例子:

比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;

如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;

案例准备

我们通过一个搜索框来延迟防抖函数的实现过程:

  • 监听input的输入,通过打印模拟网络请求

测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作:

image-20230620152534787

image-20230620152544034

underscore

Underscore库的介绍

事实上我们可以通过一些第三方库来实现防抖操作:

  • lodash

  • underscore

这里使用underscore

  • 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;

  • 但是目前我看到underscore还在维护,lodash已经很久没有更新了;

Underscore的官网: https://underscorejs.org/

安装:

Underscore的安装有很多种方式:

  • 下载Underscore,本地引入;

  • 通过CDN直接引入;

  • 通过包管理工具(npm)管理安装;

这里我们直接通过CDN:

html
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>

Underscore实现防抖和节流

image-20230620152632696

手写题

手写-防抖函数

我们按照如下思路来实现:

  • 防抖基本功能实现:可以实现防抖效果
  • 优化一:优化参数和this指向
  • 优化二:优化取消操作(增加取消功能)
  • 优化三:优化立即执行效果(第一次立即执行)
  • 优化四:优化返回值

1、基本实现

image-20230913170819191

2、优化:参数和this绑定

this指向

image-20230913171801374

参数

image-20230913172010545

image-20230913172141908

3、优化:取消功能

image-20230913173224970

image-20230913173251166

4、优化:第一次立即执行

  • immediate:控制否时启用立即执行功能
  • isInvoke:控制函数是否已经立即执行一次了

image-20230913175323462

image-20230913175525645

5、优化:返回值

image-20230914120206319

image-20230914120006631

image-20230914120016985

手写-节流函数

我们按照如下思路来实现:

  • 节流函数的基本实现:可以实现节流效果
  • 优化一:绑定this和参数
  • 优化二:控制立即执行,节流最后一次也可以执行
  • 优化三:优化添加取消功能
  • 优化四:优化返回值问题

1、基本实现

image-20230914145011970

image-20230914144933945

2、优化:绑定this和参数

image-20230914145650570

image-20230914145254694

3、优化:控制立即执行

image-20230914150800082

image-20230914145957177

4、优化:控制执行最后一次

思路一: 给每次点击时添加一个定时器,延迟时间设为waitTime,当再次点击时取消上次的定时器,重新添加一个。

思路二: 在每个执行fn函数的节点,添加一个延迟时间为waitTime的定时器,当用户在fn函数执行节点的时间上也点击了一次就取消该定时器(使用中

image-20230914161141810

4、优化:取消功能

image-20230914173427834

image-20230914173431437

5、优化:返回值

image-20230914174045131

image-20230914173857111

手写-深拷贝函数

前面我们已经学习了对象相互赋值的一些关系,分别包括:

  • 引用赋值:指向同一个对象,相互之间会影响;

  • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;

  • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;

深拷贝实现方式:

  • JSON.parse
  • 第三方库:underscore、lodash
  • 自己实现

前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse

  • 这种深拷贝的方式其实对于函数Symbol等是无法处理的;

  • 并且如果存在对象的循环引用,也会报错的;

js
const obj = JSON.parse(JSON.stringify(info))

自定义深拷贝函数:

  • 1.自定义深拷贝的基本功能;

  • 2.对Symbol的key进行处理;

  • 3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;

  • 4.对循环引用的处理;

工具函数:判断对象

image-20230915090846425

1、基本实现

image-20230915091537083

image-20230915091526589

2、优化:区分数组和对象

image-20230915092041870

image-20230915093627617

3、优化:其他类型-处理set

image-20230915095300660

image-20230915095231434

4、优化:其他类型-处理map

5、优化:其他类型-处理function

function: 不需要深拷贝

image-20230915095547885

6、优化:其他类型-处理Symbol为值

image-20230915102436519

image-20230915102117655

7、优化:其他类型-处理Symbol为key

image-20230915102503264

image-20230915102523200

8、优化:处理循环引用

方案一:将每次新创建的对象保存到Map中,每次遍历前判断之前是否已经保存过了该对象

问题:需要在deeCopy外部定义一个map,并且每次拷贝完成后map依然会形成对对象的强引用,没有销毁

image-20230915104349385

方案二(推荐):使用WeakMap替代Map;将map放入参数中并设置一个默认值new WeakMap()

image-20230915105909339

手写-事件总线

自定义事件总线属于一种观察者模式,其中包括三个角色:

  • 发布者(Publisher):发出事件(Event);

  • 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);

  • 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;

当然我们可以选择一些第三方库

  • Vue2默认是带有事件总线的功能;

  • Vue3中推荐一些第三方库,比如mitt

当然我们也可以实现自己的事件总线:

  • 事件的监听方法on;

  • 事件的发射方法emit;

  • 事件的取消监听off;

1、基本实现

image-20230915114603806

image-20230915114736411

2、优化:绑定参数

image-20230915114132235

3、优化:移除监听